"""
在之前的测试中，我们固定地将一个特定的经纬度开始的1度的区域成功地切分成100份并以osm的格式保存到hbase中(见test_grid.py)。
并且在它的基础上成功验证了使用A*算法生成航道(见route_demo.py, grid_data.py, astar.py)。
在实际运行环境中，我们需要对全球的海洋区域进行导航，所以要拓展已有的地图，让它保存所有海域的数据。
在这个脚本中，我们将提供一个实现，它以经纬度（-180.00，-90.00）作为原点分割任意区域。它生成的地图数据用于寻路算法。
后续优化：
- 两次分割的地图区域，osm id会出现重复，应当考虑分割地图时候将未用的id保存。
- 应尽量尝试多进程协同分割地图的工作，尽量使用服务器的性能。
需要在服务器上运行hbase的thrift服务器：
/usr/local/hbase/bin/hbase-daemon.sh start thrift
"""
from typing import List, Tuple
import json
import happybase
import shapely
from navieng import postgres2osm
from navieng.postgres2osm import OSMRelation, OSMLine, OSMNode
from xml.etree import ElementTree
from concurrent.futures.thread import ThreadPoolExecutor
from concurrent.futures import wait

class GridMaker:
    """
    这个类实现了以经纬度（-180.00，-90.00）作为原点分割任意区域。主要包含以下接口：
    (1)cut_parallel_auto_calculate: 执行多线程切割地图，根据指定的经纬度自动根据大小连续切割区域，每个线程最多处理100x100大小的区域。
    (2)cut_single_thread:单线程切割网格。
    (3)cut_parallel: 这个方法需要你提供坐标列表，从每个坐标开始固定切1度范围的网格（即100x100）。这个方法适用于仅更新少量区域时，不适合用于
    切割一个连续的范围，e.g 假设某经纬度开始的10度x10度，那么将需要在coord_list列出100个坐标,这样非常不方便，对于这种情况应使用
    cut_parallel_auto_calculate。

    下面是调用方法示例：
    示例用到的参数说明：
    hbase的IP和端口 hbase_host: "192.168.1.119", hbase_port: 9090
    Postgres数据库的连接配置：postgres_config: "./config/postgres.json"，文件内容如下：
    {
    "postgres":
    {
        "database": "postgres",
        "user": "gis_read",
        "password": "isd5Wp3h2sqQ",
        "host": "192.168.1.53",
        "port": "5432"
    }
    }
    起点的经纬度坐标(需0.1对齐) lon: 118.1, lat: 21.3
    需要划出多少个格子（每个格子0.01度，输入的值要能被10整除）：grid_width: 110, grid_height: 220
    需要多少个进程一起工作：process_cc: 8

    cut_parallel_auto_calculate调用示例：
    g = GridMaker(hbase_host, hbase_port, postgres_config)
    g.cut_parallel_auto_calculate(lon, lat, grid_width, grid_height, process_cc)
    同时运行process_cc个线程，从（lon,lat）开始，在x方向上切割grid_width个格子, 在y方向上切割出grid_height个格子。当process_cc<=1时
    单线程执行。每个线程至多负责切割100x100个格子，数量上不要求100对齐，数量整除100后剩余多少个格子最后的线程就处理多少。例如数量为180,100
    时，第一个线程负责格子坐标左下(0, 0)右上(100,100)的正方形区域，第二个线程负责格子坐标左下(100, 0)右上(180,100)的矩形区域。

    cut_single_thread的演示:
    g = GridMaker(hbase_host, hbase_port, postgres_config)
    g.cut_single_thread(lon, lat, grid_width, grid_height)
    单线程执行切割操作，从（lon,lat）开始，在x方向上切割grid_width个格子, 在y方向上切割出grid_height个格子。数量上不要求100对齐。

    cut_parallel使用例子:
    coord_list = [[112.1, 21.4], [113.1, 21.4], [114.1, 21.4]]
    g = GridMaker(hbase_host, hbase_port, postgres_config)
    g.cut_parallel(coord_list, process_cc)
    同时运行process_cc个线程，根据coord_list提供的坐标列表（每个元素都是（lon,lat）坐标），给每个坐标在x方向上切割100个格子, 在y方向上
    切割出100个格子。当process_cc<=1时单线程执行。
    """
    def __init__(self, hbase_host, hbase_port=10000, postgres_config="./postgres.conf"):
        self.hbase_host = hbase_host
        self.hbase_port = hbase_port
        self.postgres_config = postgres_config
        self.hbase_connection = None
        self.postgres_con = None

    @staticmethod
    def make_grid_polygons(h_cc: int, v_cc: int, lon: float, lat: float, step=0.01) -> List[List[shapely.Polygon]]:
        """
        从经纬度(lon, lat)开始，划分h_cc行v_cc列的格子。每个格子长宽均为经纬度step度.
        保存为shapely.Polygon对象的二重数组。在给postgres调用网格切割方法时提供对应格子。
        调用本函数前需已经做好了h_cc/v_cc/lon/lat的对齐。
        """
        polygons: List[List[shapely.Polygon]] = []
        for xi in range(0, h_cc):
            x_left = round(lon + xi * step, 2)
            x_right = round(x_left + step, 2)
            a_col = []
            for yi in range(0, v_cc):
                y_bottom = round(lat + yi * step, 2)
                y_top = round(y_bottom + step, 2)
                #print("polygon[{0},{1}] left,bottom = ({2}, {3}), right,top = ({4},{5})".format(xi, yi, x_left, y_bottom,
                #                                                                                x_right, y_top))
                polygon = shapely.Polygon(((x_left, y_bottom), (x_left, y_top), (x_right, y_top),
                                           (x_right, y_bottom), (x_left, y_bottom)))
                a_col.append(polygon)
            polygons.append(a_col)
        return polygons

    @staticmethod
    def list2d_to_list3d(polygons:List[List[shapely.Polygon]]) -> List[Tuple[int, int, List[List[int]]]]:
        """
        polygons: 已经生成好的，带有地理信息的格子列表。它是一个二维数组，每个元素都是一个shapely.Polygon元素。后续将用这个格子作为参数
                  取与其相交的地图。
        这个方法的作用是将(x,y)大小的二维数据改为按照10乘10的大小作为一个基本单位切成(x * y / 100)大小的数组，每个数组元素都是(10，10)
        大小的区域。
        返回 [(x0, y0, block_10_x_10), ...]这样的数组，其中x0 -> 在polygons中的x下标， y0 -> polygons中的y下标
        block_10_x_10 -> 10乘10的格子。由于是10乘10的所以x0和y0总是按10对齐，如x0: 0 10 20 30 ... , y0: 0 10 20 30 ...
        """
        assert len(polygons) % 10 == 0
        assert len(polygons[0]) % 10 == 0
        xc, yc = len(polygons) // 10, len(polygons[0]) // 10
        l3d = []
        for x in range(0, xc):
            for y in range(0, yc):
                one_block = [[0 for _ in range(0, 10)] for _ in range(0, 10)] # 一块10乘10的小区域
                for i in range(0, 10):
                    for j in range(0, 10):
                        one_block[i][j] = polygons[x*10 + i][y*10+j]
                l3d.append((x*10, y*10, one_block))
        return l3d

    def __gis_intersection_grid(self, polygon):
        # print("测试区域：", polygon.wkt)
        tables_list = [("datagis.t_lndare_r", 'r'), ("datagis.t_resare_r", 'r'),
                       ("datagis.t_wrecks_p", 'p'), ("datagis.t_obstrn_p", 'p'),
                       ("datagis.t_lndare_p", 'p'),
                       ("datagis.t_causwy_l", 'l'), ("datagis.t_slcons_l", 'l'),
                       ("datagis.t_tunnel_l", 'l'),
                       ("datagis.t_coalne_l", 'l')]
        osm_grid_map: Tuple[List[OSMNode], List[OSMLine], List[OSMRelation]] = (
            [], [], [])  # nodes, lines, relations
        for i in tables_list:
            sql = ("select "
                   "a.*, ST_AsText(ST_Intersection(a.geom, rect)) as geo "
                   "from {0} a, ST_GeomFromText('{1}', 4326) as rect "
                   "where ST_Intersects(a.geom, rect)").format(i[0], polygon.wkt)
            self.postgres_con.cursor.execute(sql)
            elements: Tuple[List[OSMNode], List[OSMLine], List[OSMRelation]] = (
                [], [], [])  # nodes, lines, relations
            while 1:
                row = self.postgres_con.cursor.fetchone()
                if not row:
                    break
                # print("Intersection row:", row)
                if i[1] == 'r':
                    (nodes, lines, relations) = postgres2osm.common_area(row, i[0])
                    elements[0].extend(nodes)
                    elements[1].extend(lines)
                    elements[2].extend(relations)
                elif i[1] == 'p':
                    osm_node = postgres2osm.common_points(row, i[0])
                    elements[0].append(osm_node)
                elif i[1] == 'l':
                    (nodes, lines, relations) = postgres2osm.common_line(row, i[0])
                    elements[0].extend(nodes)
                    elements[1].extend(lines)
                    elements[2].extend(relations)
                else:
                    raise Exception("unknown element type")
            osm_grid_map[0].extend(elements[0])  # osm_grid_map: (nodes, lines, relations)
            osm_grid_map[1].extend(elements[1])
            osm_grid_map[2].extend(elements[2])
        root = ElementTree.Element("osm", {"version": "0.6", "generator": "hand"})
        # 做个最简单最理想化的处理，不考虑桥梁等等：如果格子不为空那么认为不能通行。
        xml_text = ""
        accessible = 1  # 这个格子可到达。
        weight = 2  # 权重，当经过这个格子时增加的距离，如果这个格子不可达那么值为0。 目前只有0和1，在未来要添加上天气等因素。
        if len(osm_grid_map[0]) or len(osm_grid_map[1]) or len(osm_grid_map[2]):
            for i in osm_grid_map[0]:
                root.append(i.to_xml_element())
            for i in osm_grid_map[1]:
                root.append(i.to_xml_element())
            for i in osm_grid_map[2]:
                root.append(i.to_xml_element())
            # tree = ElementTree.ElementTree(root)
            # tree.write(test_dir+"\\{0}_{1}.xml".format(x,y), encoding="UTF-8", xml_declaration=True)
            xml_text = ElementTree.tostring(root).decode()
            accessible = 0
            weight = 0
        return xml_text, accessible, weight

    def __cut_alter_art(self, polygons:List[List[shapely.Polygon]], x_base, y_base):
        blocks = GridMaker.list2d_to_list3d(polygons)
        # blocks : [(x0, y0, block_10_x_10)] 其中x0 -> 在polygons的x下标， y0 -> polygons的y下标
        # block_10_x_10 -> 10乘10的格子。由于是10乘10的所以x0和y0总是按10对齐，如x0: 0 10 20 30 ... , y0: 0 10 20 30 ...
        for b in blocks:
            x0, y0, block_10x10 = b[0], b[1], b[2]
            if x0 % 10 != 0 or y0 % 10 != 0:
                raise Exception("逻辑错误，10x10的格子的起点x,y坐标必须是以10对齐的。")
            list_xml_text, list_accessible, list_weights, list_polygon_wkt = [], [], [], []
            for m, inner_col in enumerate(block_10x10):  # m, n: 遍历10x10大块内部
                for n, polygon in enumerate(inner_col):
                    #print(x0+m, y0+n, polygon)
                    xml_text, accessible, weight = self.__gis_intersection_grid(polygon)
                    list_xml_text.append(xml_text)
                    list_accessible.append(accessible)
                    list_weights.append(weight)
                    list_polygon_wkt.append(polygon.wkt)
            # 每个cell是10*10个小格子，x0和y0已经对齐了0.1度。
            # 保持row key的规则不变，只是现在要求row key总是能被10整除。
            table = self.hbase_connection.table('mapdata')
            x, y = x0 + x_base, y0 + y_base
            print("提交10x10的一块数据到服务器，坐标为：", x, y)
            table.put('{:016x}'.format((x << 32) + y),
                      {'route:accessible': json.dumps(list_accessible), 'route:weight': json.dumps(list_weights),
                       'geo:xml_text': json.dumps(list_xml_text), 'geo:rect': json.dumps(list_polygon_wkt)})

    def __cut_sequence(self, lon: float, lat: float, h_cc: int, v_cc: int):
        assert h_cc % 10 == 0
        assert v_cc % 10 == 0

        self.hbase_connection = happybase.Connection(host=self.hbase_host, port=self.hbase_port)
        self.postgres_con = postgres2osm.DBConnector(config_file=self.postgres_config)
        self.postgres_con.connect()

        # 小数点后1位，四舍五入。我们需要0.1度对齐，每100格子存一条。
        p_lon, p_lat = round(float(lon), 1), round(float(lat), 1)

        # 以经纬度-180.00/-90.00为原点，计算它的格子坐标。经纬度的1度对应xy坐标轴上的100个单位。
        # 防止float的精度问题，round一下
        coord_x : int = round((p_lon+180)*100)
        coord_y : int = round((p_lat+90)*100)
        # print("当前网格坐标系使用经纬度(-180.00, -90.00)作为原点，对应网格坐标区间为((0, 36000), (0, 180000))")
        # print("您希望处理的地图区域为经度{0}到{1}，纬度{2}到{3}之间的区域".format(p_lon, round(p_lon + 0.01*h_cc, 2),
        #                                                                          p_lat, round(p_lat+0.01*v_cc, 2)))
        # print("您需要分割的网格坐标起点:", coord_x, coord_y)
        # print("网格坐标的区间为:([{0},{1}),[{2},{3}))".format(coord_x, coord_x+h_cc, coord_y, coord_y+v_cc))
        # 准备用于切分网格的多边形列表
        polygons = GridMaker.make_grid_polygons(h_cc, v_cc, p_lon, p_lat)
        # 下面这几行代码用于创建坐标系保存到osm文件中，在调试的时候方便在josm中查看，在调试的时候会有点用。详见test_grid.py。
        # grid_polygons_2xml(polygons)   # 坐标系网格，用polygons更直观，可以选中某个块。
        # postgres2osm.clean_osm_elements()
        # test_grid_multi_line_strings(counter, step, min_x, min_y)  # 坐标系网格，用线更高效
        # postgres2osm.clean_osm_elements()
        # 按照polygons指定的网格切割地图，格子坐标从(coord_x, coord_y)开始
        self.__cut_alter_art(polygons, coord_x, coord_y)
        self.hbase_connection.close()
        self.postgres_con.close()

    @staticmethod
    def __check_align(lon: float, lat: float, h_cc: int, v_cc: int):
        # 小数点后1位，四舍五入。我们需要0.1度对齐，每100格子的数据合并存入hbase。
        p_lon, p_lat = round(float(lon), 1), round(float(lat), 1)
        if abs(p_lon - lon) >= 0.001:
            print("经度需要对齐到0.1度，例如应当输入117.5或117.6，而不是117.54")
            return False
        if abs(p_lat - lat) >= 0.001:
            print("纬度需要对齐到0.1度，例如应当输入19.2或19.1，而不是19.18")
            return False

        # 还是0.1对齐，一个格子是0.01所以格子数要10对齐
        if h_cc % 10 != 0:
            print("x轴方向的格子长度需要能被10整除（最小存储单元是10x10个格子）")
            return False
        if v_cc % 10 != 0:
            print("y轴方向的格子长度需要能被10整除（最小存储单元是10x10个格子）")
            return False
        return True

    def cut_single_thread(self, lon: float, lat: float, h_cc: int, v_cc: int):
        """
        单线程切割网格
        lon: 经度
        lat: 纬度
        h_cc: 水平方向上的格子数
        v_cc: 垂直方向上的格子数
        每个格子的边长是0.01度。由于规定了hbase的一条数据是10x10个格子：
        (1)每次写入hbase需要切好一百个格子后再写入数据库，为方便处理我们在这个方法中将会对lon,lat作强制
        对齐0.1度处理（四舍五入），例如经纬度(117.54, 19.18)对齐到0.1后的位置是(117.5, 19.2)。
        (2)横向和纵向长度需要是10的倍数，不能被整除时将被拓展，例如251会被拓展到260。
        """
        # 小数点后1位，四舍五入。我们需要0.1度对齐，每100格子存一条。
        if not GridMaker.__check_align(lon, lat, h_cc, v_cc):
            return
        p_lon, p_lat = round(float(lon), 1), round(float(lat), 1)
        self.__cut_sequence(p_lon, p_lat, h_cc, v_cc)

    @staticmethod
    def thread_function(lon, lat, h_cc, v_cc, hbase_host, hbase_port, postgres_config, thread_id):
        """
        用最简单的办法实现：每个线程有独立的GridMaker对象，即每个线程有独立的一份数据(数据库connection等)。
        """
        print("线程", thread_id, "开始执行，参数(", lon, lat, h_cc, v_cc, ")")
        g = GridMaker(hbase_host, hbase_port,postgres_config)
        g.__cut_sequence(float(lon), float(lat), h_cc, v_cc)
        print("线程", thread_id, "已经执行完毕。")
        return thread_id, 0

    def cut_parallel(self, coord_list, max_workers):
        """
        coord_list: 坐标列表[[lon:float, lat:float]]，在左下坐标(lon, lat),右上(lon+1度,lat+1度)围成的正方形区域内切100x100个格子
        max_workers: 工作线程数
        这个方法需要你提供坐标列表，从每个坐标开始固定切1度范围的网格（即100x100）。
        这个方法适用于仅更新少量区域时，不适合用于切割一个连续的范围，e.g 假设某经纬度开始的10度x10度，那么将需要在coord_list列出100个坐标。
        非常不方便，对于这种情况应使用cut_parallel_auto_calculate。
        """
        # 检测输入坐标是否已经对齐到0.1
        for l, i in enumerate(coord_list):
            # 小数点后1位，四舍五入。我们需要0.1度对齐，每100格子存一条。
            lon, lat = i[0], i[1]
            p_lon, p_lat = round(float(lon), 1), round(float(lat), 1)
            if abs(p_lon - lon) >= 0.01:
                print("第{0}个坐标输入错误，经度需要对齐到0.1度，例如应当输入117.5或117.6，而不是117.54".format(l+1))
                return
            if abs(p_lat - lat) >= 0.01:
                print("第{0}个坐标输入错误，纬度需要对齐到0.1度，例如应当输入19.2或19.1，而不是19.18".format(l+1))
                return
        # 不需要检查格子数对齐，因为在这个调用中固定是100x100
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = []
            thread_id = 0
            for i in coord_list:
                future = executor.submit(self.thread_function, i[0], i[1], 100, 100,
                                         self.hbase_host,
                                         self.hbase_port,
                                         self.postgres_config,
                                         thread_id)
                print("任务:", thread_id, "已提交到线程池，参数为：", i[0], i[1], ", 100, 100")
                futures.append(future)
                thread_id += 1
            # 等待所有线程执行完毕
            wait(futures)
            print("所有任务执行完毕")
            # 打印结果
            for f in futures:
                r = f.result()
                print("线程:", r[0], "，执行结果为:", r[1])
            # 或完成一个任务就处理一个任务的结果。
            # for f in as_completed(futures):
            #     r = f.result()
            #     print("线程:", r[0], "，执行结果为:", r[1])
        # 只有在cut()的时候才会连接数据库，所以我们主线程这里不用释放连接。

    def cut_parallel_auto_calculate(self, lon, lat, x_cc, y_cc, max_workers):
        """
        多线程切地图，根据lat, lon指定的经纬度，在x和y方向上切分出x_cc和y_cc个格子。每个线程最多处理100x100大小的区域。
        此函数不要求区域(即x_cc, y_cc)能被100整除但是要求能被10对齐，因为存储数据的时候10x10大小的区域存在一条数据中。此外，如果x_cc
        和y_cc不是100的整数则最后一批根据实际剩余数量切割。
        """
        if not GridMaker.__check_align(lon, lat, x_cc, y_cc):
            return

        coord_list = []  # (lat, lon ,x_cc, y_cc)
        x = 0
        while x < x_cc:
            y = 0
            cx = 100
            if x + 100 > x_cc:
                cx = x_cc - x

            while y < y_cc:
                cy = 100
                if y + 100 > y_cc:
                    cy = y_cc - y
                #print((x, y), cx, cy)
                coord_list.append((round(lon + x*0.01, 2), round(lat + y*0.01, 2), cx, cy))
                y += 100
            x += 100

        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = []
            thread_id = 0
            for i in coord_list:
                future = executor.submit(self.thread_function, i[0], i[1], i[2], i[3],
                                         self.hbase_host, self.hbase_port, self.postgres_config, thread_id)
                print("任务:", thread_id, "已提交到线程池，参数为：", i[0], i[1], i[2], i[3])
                futures.append(future)
                thread_id += 1
            # 等待所有任务完成后打印所有任务的结果。
            wait(futures)
            print("所有任务都已经执行完毕了。结果如下：")
            # 打印结果
            for f in futures:
                r = f.result()
                print("线程:", r[0], "，执行结果为:", r[1])


# if __name__ == "__main__":
#     # GridMaker.test_2d_to_4d_4x6()
#     config_dir = os.path.normpath(os.path.join(os.path.dirname(sys.argv[0]), "config"))
#     gg = GridMaker("192.168.1.39", 9090, r"D:\source\python\navigation-plan-engine\postgres2osm\test\config\postgres.json")
#     gg.cut_parallel_auto_calculate(118.06816, 24.37368, 101, 102, 8)
